"""A generic class to build line-oriented command interpreters."""

import string
IDENTCHARS = string.letters + string.digits + '_'

UnknownCommandError="cmdl.UnknownCommandError"
AmbiguousCommandError="cmdl.AmbiguousCommandError"

def listattrs(x):
	try:
		dictkeys = x.__dict__.keys()
	except (AttributeError, TypeError):
		dictkeys = []
	#
	try:
		methods = x.__methods__
	except (AttributeError, TypeError):
		methods = []
	#
	try:
		members = x.__members__
	except (AttributeError, TypeError):
		members = []
	#
	try:
		the_class = x.__class__
	except (AttributeError, TypeError):
		the_class = None
	#
	try:
		bases = x.__bases__
	except (AttributeError, TypeError):
		bases = ()
	#
	total = dictkeys + methods + members
	if the_class:
		# It's a class instace; add the class's attributes
		# that are functions (methods)...
		class_attrs = listattrs(the_class)
		class_methods = []
		for name in class_attrs:
			if is_function(getattr(the_class, name)):
				class_methods.append(name)
		total = total + class_methods
	elif bases:
		# It's a derived class; add the base class attributes
		for base in bases:
			base_attrs = listattrs(base)
			total = total + base_attrs
	total.sort()
	return total
	i = 0
	while i+1 < len(total):
		if total[i] == total[i+1]:
			del total[i+1]
		else:
			i = i+1
	return total

class CmdList:
	def __init__(self):
		self.cmdlist={}
		self.alias={}
		self.help={}
		# names are cached.
		self.names=None
	def set_alias(self,alias,name):
		self.alias[alias]=name
		self.names=None
	def add_cmd(self,name,cmd):
		self.cmdlist[name]=cmd
		self.names=None
	def find_cmd(self,name):
		return self.cmdlist[self.expand_cmd_name(name)]

	def expand_cmd_name(self,cmd_name):
		names=self.find_matching_cmds(cmd_name)
		if len(names)==1:
			name=names[0]
			if self.alias.has_key(name):
				return self.alias[name]
			return name
		elif len(names)==0:
			raise UnknownCommandError,cmd_name
		else:
			raise AmbiguousCommandError,names

	def find_matching_cmds(self,name):
		l=len(name)
		hits=[]
		for n in self.get_cmd_names():
			if name == n:
				return (name,)
			if name==n[:l]:
				hits.append(n)
		return tuple(hits)

	def get_cmd_names(self):
		if not self.names:
			self.names=self.cmdlist.keys()+self.alias.keys()
			self.names.sort()
		return self.names

	def add_help_cmd(self,name,help_cmd):
		self.help[name]=help_cmd

	def get_help_topics(self,show_aliases=1):
		help_topics={}
		# initially: use docstring
		for name,func in self.cmdlist.items():
			help_topics[name]=func.__doc__
		# add aliases
		if show_aliases:
			for name,alias in self.alias.items():
				if help_topics.has_key(alias):
					help_topics[name]=help_topics[alias]
		# ovarlay by help functions
		for name,func in self.help.items():
			help_topics[name]=func
		return help_topics
	def get_aliases(self):
		list=[]
		for name,alias in self.alias.items():
			list.append(name,alias)
		list.sort()
		return list
 
	def common_prefix(self,l):
		"""Return the longest prefix of all list elements."""
		if not l: 
			return ''
		prefix = l[0]
		for item in l:
			for i in range(len(prefix)):
				if prefix[:i+1] != item[:i+1]:
					prefix = prefix[:i]
					if i == 0: 
						return ''
					break
		return prefix

	def __repr__(self):
		return `self.get_cmd_names()`

class CmdL:
	"""A generic class to build line-oriented command interpreters.
 
	Methods starting with 'do_' are put into the command list. 
	The 'do_' is stipped from the name and the remaining part is 
	used as the command name. E.g. 'do_open' will be put as 'open' into
	the command list.

	The help is taken from the __doc__ string of each function except if
	there is a method called 'help_'. Then the string returned by this
	method is used.
	"""
	quit_cmd='quit'
	interrupt_cmd='quit'
	def __init__(self,prompt=None):
		if not prompt:
			prompt="(%s) " % self.__class__.__name__
		self.prompt = prompt
		self.identchars = IDENTCHARS
		self.lastcmd = ''
		# set the cmblist uninitialized
		self.cmdlist=None

	def split_args(self,arg):
		"""split a list of arguments"""
		args=string.split(arg)
		return args

	def make_cmdlist(self):
		"""Factory method co create a CmdList object."""
		return CmdList()

	def init_cmdlist(self):
		"""Initializes the list of commands and the available help."""
		self.cmdlist=self.make_cmdlist()
		names=listattrs(self.__class__)
		for name in names:
			if name[:3] == 'do_':
				cmd_name=name[3:]
				cmd=getattr(self,name)
				iname=cmd.im_func.__name__ # name of implementation func
				if iname != name and iname[:3]== 'do_':
					# let's assume its a alias of type do_s=do_step
					self.cmdlist.set_alias(cmd_name,iname[3:])
				else:
					self.cmdlist.add_cmd(cmd_name,cmd)
			elif name[:5] == 'help_':
				help_name=name[5:]
				cmd=getattr(self,name)
				self.cmdlist.add_help_cmd(help_name,cmd)
 
	def cmdloop(self,loop=1):
		if not self.cmdlist:
			self.init_cmdlist()
		stop = None
		while not stop:
			try:
				line = raw_input(self.prompt)
			except EOFError:
				line = self.quit_cmd
			except KeyboardInterrupt:
				line = self.interrupt_cmd
			stop = self.onecmd(line)
			if not loop:
				break
	def onecmd(self, line):
		line = string.strip(line)
		if not line:
			line = self.lastcmd
		else:
			self.lastcmd = line
		i, n = 0, len(line)
		while i < n and line[i] in self.identchars: 
			i = i+1
		cmd, arg = line[:i], string.strip(line[i:])
		if cmd == '':
			return self.default(line)
		else:
			try:
				func = self.cmdlist.find_cmd(cmd)
			except UnknownCommandError:
				return self.error_unknown_command(cmd,line)
			except AmbiguousCommandError,args:			
				return self.error_ambiguous_command(cmd,args)
			except (AttributeError,KeyError):
				return self.default(line)
			return func(arg)

	def default(self, line):
		print '*** Unknown syntax:', line

	def error_unknown_command(self,cmd,line):
		self.default(line)

	def error_ambiguous_command(self,cmd,list):
		print "*** Ambiguous command: %s %s" % (cmd,list)

	def do_help(self, arg):
		"""help
		give some help."""
		try:
			if not arg:
				self.help()
				return
			help_dict=self.cmdlist.get_help_topics()
			try:
				# let's find the full name of a function with this name
				name=self.cmdlist.expand_cmd_name(arg)
			except UnknownCommandError:
				name=arg
			except AmbiguousCommandError,args:			
				return self.error_ambiguous_command(arg,args)
			try:
				# let's find the associated help item
				doc=help_dict[name]
			except KeyError:
				return self.error_unknown_command(name)
			try:
				# it may be a help function
				doc=doc()
			except TypeError:
				pass
			self.print_help(doc,name)
		except:
			print "No help on",`arg`
	def help(self):
		"""help on help."""
		cmds_doc = []
		cmds_undoc = []
		help_only = []
		cmdlist=self.cmdlist.get_cmd_names()
		for name,help in self.cmdlist.get_help_topics().items():
			if name in cmdlist:
				if len(name)>1: # don't print shortcuts
					if help:
						cmds_doc.append(name)
					else:
						cmds_undoc.append(name)
			else:	
				help_only.append(name)
		maxcol=80
		cmdlen=max(map(len,cmds_doc+help_only+cmds_undoc))

		print
		self.print_topics("Undocumented commands:", 
							cmds_undoc,cmdlen, maxcol)
		self.print_topics(
			"Documented commands (type help <topic>):",
			cmds_doc, cmdlen, maxcol)
		self.print_topics("Miscellaneous help topics:",
							help_only, cmdlen, maxcol)
	def help_shortcuts(self):
		str=""
		for alias,cmd in self.cmdlist.get_aliases():
			str=str +"%s\t%s\n"%(alias,cmd)
		return str
	def do_shortcuts(self,arg):
		print  self.help_shortcuts()
	def print_help(self,doc,name):
		if not doc:
			print '*** No help on', `name`
		else:
			print '***',`name`
			print doc

	def print_topics(self, header, cmds, cmdlen, maxcol):
		if not cmds:
			return
		cmds=cmds[:]
		cmds.sort()
		print header;
		print "="*len(header)
		# format string
		fmt="%-"+`cmdlen`+"s"
		# for the blank between the rows:
		cmdlen=cmdlen+1 
		(cmds_per_line,junk)=divmod(maxcol,cmdlen)
		col=cmds_per_line
		for cmd in cmds:
			if col==0: 
				print
			print fmt % cmd,
			col = (col+1) % cmds_per_line
		print "\n"

def test():
	class Test(CmdL):
		def do_print(self,arg):
			print arg
		def do_plus(self,arg):
			"""Add arg."""
			print arg+arg
		def help_bla(self):
			return "Help on bla."
		def do_mult(self,arg):
			print `2*arg`
		def do_mumm(self,arg):
			"""Mumm."""
			print `4*arg`
		def do_min(self,arg):
			"""Min."""
			print `3*arg`
		def help_mult(self):
			return "Mult arg with 2"
		do_p=do_print
		do_u=do_plus
		do_m=do_min
		def do_quit(self,arg):
			return 1
	c=Test()
	c.cmdloop()
 
if __name__=='__main__':
	test()
